iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0
Software Development

異步程式設計修煉:Kotlin Coroutines 完全指南系列 第 22 篇

Day 21:StateFlow 與 SharedFlow,冷流不夠用的時候

  • 分享至 

  • xImage
  •  

Day 20:Flow 的操作符,map、filter、collect 這些常用招式 結尾點出一件事,前面兩天示範的所有 Flow,處理的都還是「每次收集就從頭開始、發送一次就結束」的那種資料流。今天要正式面對這個侷限,看看冷流在哪些情境下並不合用。

每次都從頭開始,有時候不是我要的

Day 19:從回傳一個值到回傳一串值,Flow 是什麼 定案了 Flow 的三個核心特性,依序發送、冷流、可取消。冷流指的是一個 Flow 必須等到有人收集才會運作,而且每次收集都是獨立重新跑一次的執行,沒有人收集的時候,它什麼事也不會做。這個心智模型撐起了 Day 19、Day 20 看過的所有範例,downloadProgress 也好,串接了 map、filter 的管線也好,全都遵守這個規則。

現在換個情境。假設畫面上需要隨時顯示「目前整體下載進度是多少」,不管使用者是在下載一開始就打開畫面,還是中途才切換過來查看,畫面都應該立刻看到當下正確的數值。用冷流的設計來想這件事,會發現行不通。冷流沒有人收集就不會主動運作,也不會記得任何「目前的值」,中途才開始收集的一方,只能等到下一次發送才拿得到值,前面已經發生過的進度更新,對它來說完全不存在。這種「隨時可查、跟訂閱時機無關」的需求,冷流先天做不到,需要另一種東西來補上這塊。

StateFlow,隨時可以查到的最新狀態

StateFlow 是建立在 Flow 之上的一種熱流實作,內部隨時持有一個目前最新的值。即使暫時沒有任何訂閱者在收集,這個最新值依然存在,不會因為沒人收集就停止更新或遺失。這正是它跟冷流最根本的差異:冷流沒人收集就不運作,StateFlow 不需要等待收集者出現,就能持續存在並更新自己手上那份狀態。

回到多來源圖片下載與聚合工具,如果畫面需要顯示「目前整體下載進度」,用 StateFlow 持有這個最新的進度數值再合適不過。不管畫面是一開啟就在觀察,還是中途才開始訂閱,一訂閱就能立刻拿到當下最新的進度,不需要等到下一次進度更新才有值可看。這正是 Day 19 定案的冷流做不到的事,也回應了 Day 01:為什麼你需要協程,從一個會卡住主執行緒的下載函式說起 最初的下載進度需求,如果畫面上需要隨時顯示目前最新進度,而不是只在有人主動收集時才看得到,這正是 StateFlow 適合登場的情境。

宣告一個 StateFlow 並讀取目前值,語法樣貌很簡單:

class DownloadTracker {
    private val _progress = MutableStateFlow(0)
    val progress: StateFlow<Int> = _progress

    fun updateProgress(percent: Int) {
        _progress.value = percent
    }
}

_progress.value 就是目前持有的最新值,任何地方都能直接讀取,不需要收集就能拿到當下的數字。這也讓 StateFlow 同時具備一種方向性的用途,它可以取代某些原本需要手動維護一個變數、再手動寫一套通知機制才能做到的場景。今天只建立這個方向性認識,實際的完整 API 用法留待之後有需要時再細看。

這裡可以借用一個比喻,StateFlow 像牆上一個隨時顯示目前溫度的溫度計,不管什麼時候看過去,上面都有一個當下的讀數。

SharedFlow,同一份事件,多個訂閱者一起收到

SharedFlow 同樣是建立在 Flow 之上的熱流實作,但用途跟 StateFlow 不同。SharedFlow 專注在把同一個事件廣播給所有目前正在訂閱的對象,它不持有一份「目前最新狀態」讓任何時間點加入的訂閱者都能拿到,它在意的是某個事件發生的當下,誰正在聽。

多來源圖片下載與聚合工具裡,如果某次下載過程中某個來源失敗,這個失敗事件需要通知給目前畫面上所有正在觀察這次下載的地方,例如同時顯示進度條與錯誤提示的兩個畫面元件。用 SharedFlow 廣播這個失敗事件,能讓多個訂閱者同時收到同一次事件通知:

class DownloadTracker {
    private val _errorEvents = MutableSharedFlow<String>()
    val errorEvents: SharedFlow<String> = _errorEvents

    suspend fun reportError(source: String) {
        _errorEvents.emit("來源 $source 下載失敗")
    }
}

進度條元件與錯誤提示元件各自對 errorEvents 呼叫 collect,一旦 reportError 被呼叫,兩邊都會收到同一次事件。這跟 StateFlow 持有單一狀態的定位明顯不同,這裡沒有「目前的值」這種東西可以讀,只有「這次事件發生的當下,正在訂閱的人收到了什麼」。

兩者的核心差異可以這樣對照,StateFlow 回答的是「現在的狀態是什麼」,任何時間點訂閱都拿得到目前的值;SharedFlow 回答的是「剛剛發生了什麼事」,訂閱的時機會影響能不能收到某次特定事件,錯過了就是錯過了。延續前面的溫度計比喻,SharedFlow 更像廣播電台,只有在節目播出的那個當下轉到這個頻道才會聽到,晚一步轉台就錯過了。記住這組對照,之後遇到「該用哪一個」的判斷,先問自己要的是一份隨時能查的狀態,還是一次性、跟時機綁定的事件通知,答案通常就清楚了。

熱流依然是 Flow 家族的成員

看到這裡,容易誤以為 StateFlow 跟 SharedFlow 是跟 Flow 無關的另一套新技術,這個誤解需要澄清。

StateFlow 與 SharedFlow 都是 Flow 的一種,依然可以用 Day 19、Day 20 建立的方式去收集它們,一樣呼叫 collect,一樣可以在管線裡串上 map、filter 這類操作符。收集動作依然發生在某個協程裡,依然遵守 Day 19 定案的結構化並發框架,收集所在的協程被取消,這份收集也會跟著停止。差別只在於它們不遵守冷流那種「沒人收集就不運作」的特性,而是主動、持續地存在著,不管當下有沒有訂閱者,StateFlow 手上那份最新值都在,SharedFlow 隨時準備好在事件發生時廣播出去。

這個定位值得記住的原因很直接,讀者不需要把 StateFlow、SharedFlow 當成全新的獨立知識重新學一遍,只需要在 Day 19 建立的 Flow 心智模型上,額外記住「這兩者是熱的,不是冷的」這個關鍵差異,其餘收集方式、操作符組合這些既有知識依然適用,不需要另起爐灶。

狀態與廣播事件之外,資料還能怎麼被遞交出去

今天知道了 StateFlow 適合持有一份隨時可查的最新狀態,SharedFlow 適合把同一個事件廣播給多個正在訂閱的對象,而且兩者都還是 Flow 家族的成員,收集方式與結構化並發框架依然適用。

但如果情境換成一個協程產生了一批資料,需要一筆一筆像接力一樣,直接遞交給另一個協程去處理,而不是誰想看就自己訂閱,這種需求聽起來又不太一樣。手上這批資料稱不上是需要被持有的狀態,也稱不上是廣播出去讓多個對象各自反應的事件,比較像是一個協程做完一步,就要把結果直接交到下一個協程手上,繼續往下處理。

下一篇會正式定案這個問題的答案,Channel,協程之間怎麼傳話。


上一篇
Day 20:Flow 的操作符,map、filter、collect 這些常用招式
下一篇
Day 22:Channel,協程之間怎麼傳話
系列文
異步程式設計修煉:Kotlin Coroutines 完全指南 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言